using System.Collections.Immutable;
using System.Text;
using Microsoft.CodeAnalysis;

namespace CookieCrumble.Analyzers;

[Generator]
public class SnapshotModuleGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        var rootNamespaceProvider = context.AnalyzerConfigOptionsProvider
            .Select((options, _) =>
            {
                options.GlobalOptions.TryGetValue("build_property.RootNamespace", out var rootNamespace);
                return string.IsNullOrWhiteSpace(rootNamespace) ? "CookieCrumble" : rootNamespace;
            });

        var compilationProvider = context.CompilationProvider;

        var providerInterfaceProvider = compilationProvider
            .Select((comp, _) => comp.GetTypeByMetadataName("CookieCrumble.ISnapshotModule"));

        var providerTypes = compilationProvider
            .Combine(providerInterfaceProvider)
            .SelectMany((tuple, _) =>
            {
                var (comp, providerInterface) = tuple;
                if (providerInterface is null)
                {
                    return ImmutableArray<INamedTypeSymbol>.Empty;
                }

                var providers = GetProviderTypes(comp.GlobalNamespace, providerInterface);
                return providers.ToImmutableArray();
            })
            .Collect();

        var combined = providerTypes.Combine(rootNamespaceProvider);

        context.RegisterSourceOutput(combined, (spc, tuple) =>
        {
            var (types, rootNamespace) = tuple;
            var source = GenerateModuleInitializerClass(types, rootNamespace);
            spc.AddSource("ModuleInitializer.g.cs", source);
        });
    }

    private static IEnumerable<INamedTypeSymbol> GetProviderTypes(
        INamespaceSymbol namespaceSymbol,
        INamedTypeSymbol providerInterface)
    {
        foreach (var member in namespaceSymbol.GetMembers())
        {
            if (member is INamespaceSymbol ns)
            {
                foreach (var nestedType in GetProviderTypes(ns, providerInterface))
                {
                    if (IsProvider(nestedType, providerInterface))
                    {
                        yield return nestedType;
                    }
                }
            }
            else if (member is INamedTypeSymbol type)
            {
                if (IsProvider(type, providerInterface))
                {
                    yield return type;
                }

                foreach (var nested in type.GetTypeMembers())
                {
                    foreach (var nestedType in GetAllNamedTypes(nested))
                    {
                        if (IsProvider(nestedType, providerInterface))
                        {
                            yield return nestedType;
                        }
                    }
                }
            }
        }

        static bool IsProvider(INamedTypeSymbol type, INamedTypeSymbol providerInterface)
            => type.TypeKind == TypeKind.Class
                && !type.IsAbstract
                && type.AllInterfaces.Contains(providerInterface, SymbolEqualityComparer.Default);
    }

    private static IEnumerable<INamedTypeSymbol> GetAllNamedTypes(INamedTypeSymbol typeSymbol)
    {
        yield return typeSymbol;

        foreach (var nested in typeSymbol.GetTypeMembers())
        {
            foreach (var nestedType in GetAllNamedTypes(nested))
            {
                yield return nestedType;
            }
        }
    }

    private static string GenerateModuleInitializerClass(
        ImmutableArray<INamedTypeSymbol> providerTypes,
        string? rootNamespace)
    {
        rootNamespace ??= "CookieCrumble";

        var sb = new StringBuilder();
        sb.AppendLine("// <auto-generated />");
        sb.AppendLine("using System;");
        sb.AppendLine("using System.Runtime.CompilerServices;");
        sb.AppendLine();
        sb.AppendLine($"namespace {rootNamespace}");
        sb.AppendLine("{");
        sb.AppendLine("    internal static partial class ModuleInitializer");
        sb.AppendLine("    {");
        sb.AppendLine("        [ModuleInitializer]");
        sb.AppendLine("        public static void Initialize()");
        sb.AppendLine("        {");

        var count = 1;
        foreach (var t in providerTypes)
        {
            var providerName = "provider" + count++;
            sb.AppendLine($"            var {providerName} = new {t.ToDisplayString(format: SymbolDisplayFormat.FullyQualifiedFormat)}();");
            sb.AppendLine($"            {providerName}.Initialize();");
        }

        sb.AppendLine("            OnInitialize(global::CookieCrumble.Snapshot.RegisterFormatter);");
        sb.AppendLine("        }");
        sb.AppendLine();
        sb.AppendLine("        static partial void OnInitialize(global::System.Action<global::CookieCrumble.Formatters.ISnapshotValueFormatter> register);");
        sb.AppendLine("    }");
        sb.AppendLine("}");
        return sb.ToString();
    }
}
